package server

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime/debug"
	"strings"
	"time"

	"github.com/signal18/replication-manager/config"
	"github.com/signal18/replication-manager/utils/s18log"
	apilog "github.com/sirupsen/logrus"
	log "github.com/sirupsen/logrus"
	"gopkg.in/natefinch/lumberjack.v2"
)

// State Levels
const (
	StateWarn = "WARNING"
	StateErr  = "ERROR"
)

/*
This function is for printing log based on module log level
set forcingLog = true if you want to force print
*/
func (repman *ReplicationManager) LogModulePrintf(forcingLog bool, module int, level string, format string, args ...interface{}) int {
	line := 0
	stamp := fmt.Sprint(time.Now().Format("2006/01/02 15:04:05"))
	padright := func(str, pad string, lenght int) string {
		for {
			str += pad
			if len(str) > lenght {
				return str[0:lenght]
			}
		}
	}

	tag := config.GetTagsForLog(module)
	cliformat := format
	format = "[monitor] [" + tag + "] " + padright(level, " ", 5) + " - " + format

	eligible := repman.Conf.IsEligibleForPrinting(module, level)
	//Write to htlog and tlog
	if eligible || forcingLog {
		// line = repman.LogPrintf(level, format, args...)
		if repman.tlog.Len > 0 {
			repman.tlog.Add(fmt.Sprintf(format, args...))
		}

		if repman.Conf.HttpServ {
			httpformat := fmt.Sprintf("[%s] %s", tag, cliformat)
			msg := s18log.HttpMessage{
				Group:     "none",
				Level:     level,
				Timestamp: stamp,
				Text:      fmt.Sprintf(httpformat, args...),
			}
			line = repman.Logs.Add(msg)
		}

		if repman.Conf.Daemon {
			// wrap logrus levels
			switch level {
			case "ERROR":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "log", "module": tag}).Errorf(cliformat, args...)
			case "INFO":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "log", "module": tag}).Infof(cliformat, args...)
			case "DEBUG":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "log", "module": tag}).Debugf(cliformat, args...)
			case "WARN":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "log", "module": tag}).Warnf(cliformat, args...)
			case "TEST":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "test", "channel": "StdOut", "module": tag}).Infof(cliformat, args...)
			case "BENCH":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "benchmark", "channel": "StdOut", "module": tag}).Infof(cliformat, args...)
			case "ALERT":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "alert", "channel": "StdOut", "module": tag}).Errorf(cliformat, args...)
			case "START":
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "alert", "channel": "StdOut", "module": tag}).Warnf(cliformat, args...)
			case "STATE":
				status := cliformat[0:6]
				code := cliformat[7:15]
				err := cliformat[18:]
				if status == "OPENED" {
					repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "state", "status": status, "code": code, "channel": "StdOut"}).Warnf(err, args...)
				} else {
					repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "state", "status": status, "code": code, "channel": "StdOut"}).Warnf(err, args...)
				}

			default:
				repman.Logrus.WithFields(log.Fields{"cluster": "none", "type": "log", "module": tag}).Printf(cliformat, args...)
			}
		}
	}

	return line
}

func (repman *ReplicationManager) LogPanicToFile() {
	if r := recover(); r != nil {
		fields := log.Fields{
			"cluster":    "none",
			"panic":      r,
			"stacktrace": string(debug.Stack()),
		}

		repman.Logrus.WithFields(fields).Print("Application terminated unexpectedly")

		// Convert to json
		path := filepath.Join(repman.Conf.WorkingDir, "panic.log")
		content, err := json.MarshalIndent(fields, "", "\t")
		if err != nil {
			repman.Logrus.Print("Unable to decode stacktrace")
		}

		if err = os.WriteFile(path, content, 0644); err != nil {
			repman.Logrus.Print("Unable to write stacktrace to panic.log")
		}

	}
}

// Function to update the log level for the RotateFileHook
func (repman *ReplicationManager) UpdateFileHookLogLevel(hook *s18log.RotateFileHook, newLogLevel int) error {
	// Update the log level in the hook's configuration
	hook.Config.Level = config.ToLogrusLevel(newLogLevel)

	// Remove the existing hook from the logger
	allhooks := repman.Logrus.Hooks
	for level, hooks := range allhooks {
		for i, h := range hooks {
			if h == hook {
				if len(hooks) == 1 {
					// If this is the only hook for this level, remove the level from allhooks
					delete(allhooks, level)
				} else {
					allhooks[level] = append(hooks[:i], hooks[i+1:]...)
				}
				break
			}
		}
	}

	// Add the updated hook back to the logger
	for _, level := range hook.Levels() {
		allhooks[level] = append(allhooks[level], hook)
	}

	// Replace the hooks in the logger with the updated ones
	// This is necessary to ensure the logger uses the updated hook configuration
	repman.Logrus.ReplaceHooks(allhooks)

	stamp := fmt.Sprint(time.Now().Format("2006/01/02 15:04:05"))
	text := fmt.Sprintf("File log level changed successfully to %s", hook.Config.Level.String())

	if repman.tlog.Len > 0 {
		repman.tlog.Add(text)
	}

	if repman.Conf.HttpServ {
		msg := s18log.HttpMessage{
			Group:     "none",
			Level:     "INFO",
			Timestamp: stamp,
			Text:      text,
		}

		for _, cl := range repman.Clusters {
			cl.Log.Add(msg)
		}
	}

	repman.Logrus.WithFields(log.Fields{"new_file_log_level": hook.Config.Level.String()}).Info(text)

	return nil
}

// checkAndRotateLog checks if the log file has any content before rotating
func (repman *ReplicationManager) CheckAndRotateLog(logFile *lumberjack.Logger, u *user.User) {
	defer repman.LogPanicToFile()
	fileInfo, err := os.Stat(logFile.Filename)
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Println("Log file does not exist, no rotation needed.")
			return
		}
		fmt.Println("Error checking log file:", err)
		return
	}

	// Only rotate if the file has content (size > 0)
	if fileInfo.Size() > 0 {
		err := logFile.Rotate()
		if err != nil {
			fmt.Println("Failed to rotate log file:", err)
		} else {
			exec.Command("chown", fmt.Sprintf("%s:%s", u.Uid, u.Gid), logFile.Filename).Run()
			fmt.Println("Log file rotated.")
		}
	} else {
		fmt.Println("Log file is empty, no rotation performed.")
	}
}

// ApiLogAdapter is a custom log adapter for http server using logrus
// If suppress error enabled, it will suppress the log message if the hash is already written to the log more than MaxErrorWrite
// The hash will be generated from the formatted message
// The hash will be stored in the SuppressHashList
// The hash will be reset after the ResetDuration
type ApiLogAdapter struct {
	logger           *apilog.Logger
	SuppressError    bool                     // Suppress error after MaxErrorWrite
	SuppressHashList map[string]*LogHashEntry // Suppress hash list
	MaxErrorWrite    int                      // Maximum number of error write to log with same hash
	ResetDuration    time.Duration            // Reset duration for hash list
	DisregardPort    bool                     // Disregard port number, default is true
}

type LogHashEntry struct {
	Hash      string
	Printed   int
	Timestamp time.Time
}

// NewApiLogAdapter creates a new ApiLogAdapter. Default setting is not to suppress error, with MaxErrorWrite of 5, and ResetDuration of 1 minute based on configuration
// Default values:
// SuppressError: false
// MaxErrorWrite: 5
// ResetDuration: 1 minutes
// DisregardPort: true
func NewApiLogAdapter(supress bool, limit, duration int, groupby bool) *ApiLogAdapter {
	return &ApiLogAdapter{
		logger:           apilog.New(),
		SuppressError:    supress,
		MaxErrorWrite:    limit,
		SuppressHashList: make(map[string]*LogHashEntry),
		ResetDuration:    time.Duration(duration) * time.Minute,
		DisregardPort:    groupby,
	}
}

func (la *ApiLogAdapter) SetOutput(w *os.File) {
	la.logger.SetOutput(w)
}

func (la *ApiLogAdapter) SetFormatter(f apilog.Formatter) {
	la.logger.SetFormatter(f)
}

func (la *ApiLogAdapter) SetLevel(l apilog.Level) {
	la.logger.SetLevel(l)
}

func (la *ApiLogAdapter) SetSuppressError(b bool) {
	la.SuppressError = b
}

func (la *ApiLogAdapter) SetMaxErrorWrite(n int) {
	la.MaxErrorWrite = n
}

func (la *ApiLogAdapter) SetResetDuration(d time.Duration) {
	la.ResetDuration = d
}

func (la *ApiLogAdapter) SetDisregardPort(b bool) {
	la.DisregardPort = b
}

// stripPort checks if the message contains "error" and removes ports if found
func stripPort(message string) string {

	// Step 2: Remove ports from IP addresses and domain names
	// Regex to match IPs or domains with ports
	pattern := `\b((?:\d{1,3}\.){3}\d{1,3}|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}):\d{1,5}\b`
	re := regexp.MustCompile(pattern)

	// Replace matched IPs/domains with ports removed
	processedMessage := re.ReplaceAllString(message, "$1")

	return processedMessage
}

// Write writes the log message to logger without any modification
// If suppress error enabled, it will suppress the log message if the hash is already written to the log more than MaxErrorWrite
// The hash will be generated from the formatted message
// The hash will be stored in the SuppressHashList
// The hash will be reset after the ResetDuration
func (la *ApiLogAdapter) Write(p []byte) (n int, err error) {
	message := string(p)

	// Step 1: Check if the message contains "error" (case-insensitive)
	if strings.Contains(strings.ToLower(message), "error") && la.SuppressError {
		if la.DisregardPort {
			message = stripPort(message)
		}

		md5hash := md5.New()
		md5hash.Write([]byte(message))
		md5sum := fmt.Sprintf("%x", md5hash.Sum(nil))

		entry, ok := la.SuppressHashList[md5sum]
		if ok {
			n := entry.Printed

			// Reset the counter if the duration is exceeded
			if time.Since(entry.Timestamp) > la.ResetDuration {
				entry.Printed = 0
				entry.Timestamp = time.Now()
			}

			if n >= la.MaxErrorWrite {
				return 0, nil
			}
		} else {
			entry = &LogHashEntry{
				Hash:      md5sum,
				Printed:   0,
				Timestamp: time.Now(),
			}

			la.SuppressHashList[md5sum] = entry
		}

		entry.Printed++
	}

	la.logger.Out.Write(p)

	return len(p), nil
}
